package grpcsentry

import (
	"context"
	"fmt"

	"gitee.com/xuyiping_admin/pkg/xerr"

	"github.com/getsentry/sentry-go"
	grpcMiddleware "github.com/grpc-ecosystem/go-grpc-middleware"
	ctxTag "github.com/grpc-ecosystem/go-grpc-middleware/tags"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

var (
	defaultOptions = &options{
		reportDecider: defaultReportDecider,
		rePanic:       false,
	}
)

type options struct {
	reportDecider func(err error) bool
	rePanic       bool
}

func evaluateServerOpt(opts []Option) *options {
	optCopy := &options{}
	*optCopy = *defaultOptions
	for _, o := range opts {
		o(optCopy)
	}
	return optCopy
}

type Option func(*options)

// WithCodes customizes the function for mapping errors to error codes.
func WithReportDecider(f func(err error) bool) Option {
	return func(o *options) {
		o.reportDecider = f
	}
}

func WithRePanic(v bool) Option {
	return func(o *options) {
		o.rePanic = v
	}
}

func defaultReportDecider(err error) bool {
	_, isCustom := xerr.IsCustomError(err)
	if isCustom {
		return false
	}
	code := status.Code(err)
	switch code {
	case codes.Unknown, codes.Unimplemented, codes.Internal, codes.DeadlineExceeded, codes.DataLoss:
		return true
	default:
		return false
	}
}

// WithUnaryServerHandler intercept unary grpc handler and report error to sentry
func WithUnaryServerHandler(opts ...Option) grpc.UnaryServerInterceptor {
	o := evaluateServerOpt(opts)
	return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
		var hub *sentry.Hub
		hub, ctx = newHubForCall(ctx, info.FullMethod)
		hub.Scope().SetExtra("request", req)
		// Recover and capture panic
		defer func(ctx context.Context) {
			if rval := recover(); rval != nil {
				capturePanicWithContext(ctx, rval)
				if o.rePanic {
					panic(rval)
				}
				err = status.Error(codes.Internal, fmt.Sprint(rval))
			}
		}(ctx)

		resp, err = handler(ctx, req)
		if o.reportDecider(err) {
			reportError(ctx, err)
		}
		return resp, err
	}
}

// WithStreamServerHandler intercept stream grpc handler and report error to sentry
func WithStreamServerHandler(opts ...Option) grpc.StreamServerInterceptor {
	o := evaluateServerOpt(opts)
	return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) (err error) {
		_, newCtx := newHubForCall(stream.Context(), info.FullMethod)
		wrappedStream := grpcMiddleware.WrapServerStream(stream)
		wrappedStream.WrappedContext = newCtx
		stream = wrappedStream
		// Recover and capture panic
		defer func(ctx context.Context) {
			if rval := recover(); rval != nil {
				capturePanicWithContext(ctx, rval)
				if o.rePanic {
					panic(rval)
				}
				err = status.Error(codes.Internal, fmt.Sprint(rval))
			}
		}(stream.Context())

		err = handler(srv, stream)
		if o.reportDecider(err) {
			reportError(stream.Context(), err)
		}

		return err
	}
}

// report if err != nil
func reportError(ctx context.Context, err error) {
	errCode := status.Code(err)
	hub := sentry.GetHubFromContext(ctx)
	if hub == nil {
		return
	}

	sentryExtras := ctxTag.Extract(ctx).Values()
	sentryTags := make(map[string]string)
	sentryTags["grpc.code"] = errCode.String()

	hub.ConfigureScope(func(scope *sentry.Scope) {
		scope.SetTags(sentryTags)
		scope.SetExtras(sentryExtras)
	})
	hub.CaptureException(err)
}

func capturePanicWithContext(ctx context.Context, err interface{}) {
	hub := sentry.GetHubFromContext(ctx)
	if hub == nil {
		return
	}
	hub.ConfigureScope(func(scope *sentry.Scope) {
		scope.SetExtras(ctxTag.Extract(ctx).Values())
	})
	_ = hub.RecoverWithContext(ctx, err)
}

func newHubForCall(ctx context.Context, fullMethodString string) (*sentry.Hub, context.Context) {
	hub := sentry.CurrentHub().Clone()
	hub.ConfigureScope(func(scope *sentry.Scope) {
		scope.SetTag("grpc_method", fullMethodString)
	})
	newCtx := sentry.SetHubOnContext(ctx, hub)
	return hub, newCtx
}
